Springboot+SpringSecurity+JWT +Vue實作前后端分離的登錄 權限管理以及token管理
前言
使用SpringSecurity+jwt組合對用戶的權限以及token進行管理,用戶在成功登陸后,會分發一個token令牌,如果沒有將某個介面進行權限設定,那么訪問該頁面時需要帶著token令牌進行訪問,后端將會對token令牌進行校驗,如果校驗通過則可以對該介面進行請求,
整體思路:
1.匯入springSecurity跟jwt的maven依賴
2.用戶登錄的物體類(這里就不詳細說明mapper和service)
3.實作UserDetailsService介面
4.JwtUser實作UserDetails介面
5.JwtTokenUtils工具類
6.驗證用戶登錄資訊的攔截器(JWTAuthenticationFilter)
7.驗證用戶權限的攔截器(JWTAuthorizationFilter)
8.springSecurity配置
9.認證的Controller
10.前端vue進行測驗(跨域可以在博主的其他文章中查看)
1.匯入springSecurity跟jwt的maven依賴
<!--springSecuriy-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2.用戶登錄的物體類
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="User物件", description="")
public class User {
private static final long serialVersionUID = 1L;
@TableId(value = "uid", type = IdType.AUTO)
private Long uid;
private String username;
private String password;
private String role;
}
3.實作UserDetailsService介面
這里使用的是mybatisplus對資料庫進行訪問,在博主其他文章中也有筆記
@Service
public class UserServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
// 用戶登錄邏輯和驗證處理
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//將用戶傳來的名字進行后綴判斷角色
JwtUser jwtUser = null;
QueryWrapper<Student> wrapper = new QueryWrapper<>();
wrapper.eq("username",s);
User user = studentMapper.selectOne(wrapper);
return new JwtUser(user);
}
}
4.JwtUser實作UserDetails介面
public class JwtUser implements UserDetails {
private Long id;
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;
public JwtUser() {
}
public JwtUser(User user) {
id = user.getUid();
username = user.getUsername();
password = user.getPassword();
//這里說明一下,必須要加上ROLE_開頭,或者在資料庫直接以這個開頭
authorities = Collections.singleton(new SimpleGrantedAuthority("ROLE_"+user.getRole()));
}
// 獲取權限資訊
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
// 賬號是否未過期,默認是false,記得要改一下
@Override
public boolean isAccountNonExpired() {
return true;
}
// 賬號是否未鎖定,默認是false,記得也要改一下
@Override
public boolean isAccountNonLocked() {
return true;
}
// 賬號憑證是否未過期,默認是false,記得還要改一下
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
5.JwtTokenUtils工具類
用于設定token過期時間,以及token的前綴等
public class JwtTokenUtils {
public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
private static final String SECRET = "jwtsecretdemo";
private static final String ISS = "echisan";
// 角色的key
private static final String ROLE_CLAIMS = "rol";
// 過期時間是3600秒,既是1個小時
private static final long EXPIRATION = 3600L;
// 創建token
public static String createToken(String username,String role) {
long expiration =EXPIRATION;
HashMap<String, Object> map = new HashMap<>();
map.put(ROLE_CLAIMS, role);
return Jwts.builder()
.signWith(SignatureAlgorithm.HS512, SECRET)
.setClaims(map)
.setIssuer(ISS)
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.compact();
}
// 從token中獲取用戶名
public static String getUsername(String token){
return getTokenBody(token).getSubject();
}
// 獲取用戶角色
public static String getUserRole(String token){
return (String) getTokenBody(token).get(ROLE_CLAIMS);
}
// 是否已過期
public static boolean isExpiration(String token) {
try {
return getTokenBody(token).getExpiration().before(new Date());
} catch (ExpiredJwtException e) {
return true;
}
}
private static Claims getTokenBody(String token){
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
}
}
6.驗證用戶登錄資訊的攔截器(JWTAuthenticationFilter)
//進行用戶賬號的驗證
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
//設定登錄請求的url
super.setFilterProcessesUrl("/login");
}
@SneakyThrows
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException{
// 從輸入流中獲取到登錄的資訊
try {
LoginUser loginUser = new ObjectMapper().readValue(request.getInputStream(), LoginUser.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword(), new ArrayList<>()));
//密碼錯誤時拋出例外
}catch (BadCredentialsException b){
System.out.println("密碼錯誤");
try {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("密碼錯誤");
} catch (IOException e) {
e.printStackTrace();
}
return null;
//無此用戶時拋出例外
}catch (InternalAuthenticationServiceException i){
System.out.println("沒有此用戶");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("xxx");
return null;
}
catch (Exception e) {
e.printStackTrace();
return null;
}
}
// 成功驗證后呼叫的方法
// 如果驗證成功,就生成token并回傳
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
System.out.println("登錄成功");
JwtUser jwtUser = (JwtUser) authResult.getPrincipal();
System.out.println("jwtUser:" + jwtUser.toString());
String role = "";
Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
for (GrantedAuthority authority : authorities) {
role = authority.getAuthority();
}
String token = JwtTokenUt)ils.createToken(jwtUser.getUsername(), role);
// 回傳創建成功的token
// 但是這里創建的token只是單純的token
// 按照jwt的規定,最后請求的時候應該是 `Bearer token`
System.out.println(token);
response.setHeader("token", JwtTokenUtils.TOKEN_PREFIX + token);
//這里我還將該用戶的id進行回傳了
response.setIntHeader("uid",jwtUser.getUid().intValue());
}
//配置失敗回傳的資訊,當然成功的也可以回傳思路一樣,重寫successful,這里我就不寫了
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
System.out.println("用戶登陸失敗 AjaxAuthFailHandler");
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
PrintWriter out = response.getWriter();
out.write("{\"status\":\"error\",\"msg\":\"請檢查用戶名、密碼或驗證碼是否正確\"}");
out.flush();
out.close();
}
}
7.驗證用戶權限的攔截器(JWTAuthorizationFilter)
進行用戶賬號的驗證(查看是否有token,token是否正確,token是否過時)
//進行用戶賬號的驗證
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
String tokenHeader = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
// 如果請求頭中沒有Authorization資訊則直接放行了
if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtils.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
// 如果請求頭中有token,則進行決議,并且設定認證資訊
try {
SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
} catch (TokenIsExpiredException e) {
//回傳json形式的錯誤資訊
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
String reason = "統一處理,原因:" + e.getMessage();
response.getWriter().write(new ObjectMapper().writeValueAsString(reason));
response.getWriter().flush();
return;
}
super.doFilterInternal(request, response, chain);
}
// 這里從token中獲取用戶資訊并新建一個token
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) throws TokenIsExpiredException {
String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, "");
boolean expiration = JwtTokenUtils.isExpiration(token);
if (expiration) {
throw new TokenIsExpiredException("token超時了");
} else {
String username = JwtTokenUtils.getUsername(token);
String role = JwtTokenUtils.getUserRole(token);
if (username != null) {
return new UsernamePasswordAuthenticationToken(username, null,
Collections.singleton(new SimpleGrantedAuthority(role))
);
}
}
return null;
}
}
8.springSecurity的配置
@Configuration //配置類
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConf extends WebSecurityConfigurerAdapter {
@Autowired
UserServiceImpl userService;
//定義登陸成功回傳資訊
private class AjaxAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("用戶[" + SecurityContextHolder.getContext().getAuthentication().getPrincipal() +"]登陸成功!");
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("{\"status\":\"ok\",\"msg\":\"登錄成功\"}");
out.flush();
out.close();
}
}
//定義登陸失敗回傳資訊
private class AjaxAuthFailHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
System.out.println("用戶登陸失敗 AjaxAuthFailHandler");
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
PrintWriter out = response.getWriter();
out.write("{\"status\":\"error\",\"msg\":\"請檢查用戶名、密碼或驗證碼是否正確\"}");
out.flush();
out.close();
}
}
//請求授權驗
@Override
protected void configure(HttpSecurity http) throws Exception {
// .denyAll(); //拒絕訪問
// .authenticated(); //需認證通過
// .permitAll(); //無條件允許訪問
// 跨域一定要加上csrf();
// 訪問權限
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers("/tologin","/islogin","/register","/registererr").permitAll()
//.antMatchers("/User").hasRole("USER")
// .antMatchers("/Admin").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
//不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//登錄配置
http.formLogin()
.successHandler(new AjaxAuthSuccessHandler())
.failureHandler(new AjaxAuthFailHandler())
//固定名字,其實默認也是username跟password,這里也可以不寫
.usernameParameter("username")
.passwordParameter("password")
//設定自己的登錄界面(如果不設定,將會是自帶的登錄界面這里我們使用自定義的登錄界面)
.loginPage("/tologin")
//表單提交的url
.loginProcessingUrl("/login");
}
// 用戶授權驗證
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userService)
.passwordEncoder(passwordEncoder()
);
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
// 這里采用雪花加密方式對密碼進行加密解密
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
9.認證的Controller
//單跨域的使用
@CrossOrigin
@Controller
public class PageController {
@GetMapping("/tologin")
public String tologin(){
return "redirect:http://localhost:8001/Login";
}
}
10.前端vue進行測驗
這里需要有一點vue的基礎, Vue這里不會詳細講用法了,但是會講我是如何保存token,攜帶token發送請求的,這里我發送請求用的是axios,關于vue跨域配置等后期可能會寫一篇相關的文章,
Login.vue
只給到了發送請求的地方,這里我將得到的token以及uid都存放到session中
var data = JSON.stringify(that.loginForm);
console.log(that.loginForm.username)
this.$axios
.post(url, data)
.then(function (res) {
//登錄成功后,將后端給的token以及uid都存在session中
window.sessionStorage.setItem("token", res.headers.token);
window.sessionStorage.setItem("uid",res.headers.uid);
that.$message.success("登陸成功");
}).catch((err) => {
//賬號或密碼有誤時
var code = err.response.status;
return this.$message.error("登錄失敗!賬號或密碼錯誤");
});
main.js
這里從session中獲取token,并放到axios的請求頭中,后面使用axios的請求都會自動帶上token
//vue 的main.js中配置
//給axios請求設定默認的token請求頭
const token = window.sessionStorage.getItem("token");
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8';
axios.defaults.headers['Authorization'] = token;
axios.defaults.headers.post['Content-Type'] = 'text/plain';
Vue.prototype.$axios = axios;
記錄我的學習筆記
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/267165.html
標籤:其他
