先參考文章https://blog.csdn.net/grd_java/article/details/107584578,了解springSecurity基本操作,另外,資料庫也在這篇文章創建了
文章目錄
一、環境說明(太基本的就不講了,本文主要講如何前后端分離模式整合Security) 二、配置Security 1. 用戶物體類繼承UserDetails介面 2. 撰寫JWT工具類 3. 配置JWT過濾器 4. 自定義回傳結果 5. Security配置類
三、實作登錄邏輯 1. 專門給登錄用戶的物體類 2. controller 3. service
四、通過swagger測驗 1. 配置swagger,攜帶指定請求頭authorization,規定哪些路徑需要認證(只有規定的路徑才會攜帶請求頭) 2. 測驗
五、驗證碼實作
六、實作權限管理系統
請求login介面,獲取token 獲取token后,通過封裝名為Authorization的請求頭,值為Bearer+空格+token字串的形式請求需要授權介面(就是請求時,需要帶上authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE2Mzk0NjA2MTYsImV4cCI6MTYzOTQ2MTMwMCwic3ViIjoiYWRtaW4iLCJjcmVhdGVkIjoxNjM5NDYwNjE2MTI0fQ._oKG2qarbKi84gxdQjHoRSHd3hx-INQn1CscMgAiASW0B6tsPIjWi1LMr35OTtWR-WvQ8R6tRAvkp0Q3RQm0LQ這樣的請求頭)
authorization是請求頭key Bearer 是我們的一個頭標識 剩下的就是token字串
@TODO 因為要將springSecurity整個的權限控制流程都涉及到,暫時還沒寫完,寫完會上傳到倉庫
一、環境說明(太基本的就不講了,本文主要講如何前后端分離模式整合Security)
common除了Spring security模塊外,都在這篇文章中有講解https://blog.csdn.net/grd_java/article/details/107452826 不知道怎么建表,請到文章開頭的鏈接中參考
二、配置Security
首先,我們在yml中需要配置一些引數,是我們個人規定好的
我們規定相應的token 請求頭為authorization:Bearer token字串
jwt :
tokenHeader : Authorization #JWT存盤的請求頭,請求是這個就是token的key
secret : ukc8BDbRigUDaY6pZFfWus2jZWLPHO #JWT加解密使用的密鑰
expiration : 684808 #JWT有效時間(60*60*24)
tokenHead : Bearer #JWT負載中拿到的開頭,token字串,tokenHead是字串的頭,也就是以它開頭
1. 用戶物體類繼承UserDetails介面
import com. baomidou. mybatisplus. annotation. IdType ;
import com. baomidou. mybatisplus. annotation. TableId ;
import io. swagger. annotations. ApiModel ;
import io. swagger. annotations. ApiModelProperty ;
import lombok. Data ;
import lombok. EqualsAndHashCode ;
import lombok. experimental. Accessors ;
import org. springframework. security. core. GrantedAuthority ;
import org. springframework. security. core. userdetails. UserDetails ;
import java. util. Collection ;
@Data
@EqualsAndHashCode ( callSuper = false )
@Accessors ( chain = true )
@ApiModel ( value= "DdUser物件,使用Spring Security框架就要繼承UserDetails介面,實作方法,將回傳值改為true" , description= "" )
public class DdUser implements UserDetails {
private static final long serialVersionUID= 1L ;
@ApiModelProperty ( value = "id" )
@TableId ( value = "id" , type = IdType . ID_WORKER_STR)
private Integer id;
private String username;
private String password;
/**
* 所有權限
* @return
*/
@Override
public Collection < ? extends GrantedAuthority > getAuthorities ( ) {
return null ;
}
/**
* 賬號是否過期
* @return
*/
@Override
public boolean isAccountNonExpired ( ) {
return true ;
}
/**
* 賬號是否被鎖定
* @return
*/
@Override
public boolean isAccountNonLocked ( ) {
return true ;
}
/**
* 憑證(密碼)是否過期
* @return
*/
@Override
public boolean isCredentialsNonExpired ( ) {
return true ;
}
/**
* 是否啟用
* @return
*/
@Override
public boolean isEnabled ( ) {
return true ;
}
}
2. 撰寫JWT工具類
import io. jsonwebtoken. Claims ;
import io. jsonwebtoken. Jwts ;
import io. jsonwebtoken. SignatureAlgorithm ;
import org. springframework. beans. factory. annotation. Value ;
import org. springframework. security. core. userdetails. UserDetails ;
import org. springframework. stereotype. Component ;
import org. springframework. util. StringUtils ;
import java. util. Date ;
import java. util. HashMap ;
import java. util. Map ;
/**
* JWT工具類
*/
@Component
public class JwtTokenUtil {
private static final String CLAIM_KEY_USERNAME= "sub" ; //荷載,用戶名的key
private static final String CLAIM_KEY_CREATED= "created" ; //荷載,創建時間key
/**
* 專案中推薦使用此形式的密鑰和過期時間,因為解耦合
*/
@Value ( "${jwt.secret}" )
private String secret; //密鑰
@Value ( "${jwt.expiration}" )
private Long expiration; //過期時間
/**
* 不推薦使用這種,因為寫死到了代碼中
*/
public static final long EXPIRE = 1000 * 60 * 60 * 24 ; //設定token過期時間
public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO" ; //設定token密鑰,我瞎寫的,每個公司都有按自己規則生成的密鑰
//生成token字串
/**
* 根據荷載生成token字符
* @param id 密言1
* @param nickname 密言2
* @return
*/
public static String getJwtToken ( String id, String nickname) {
String JwtToken = Jwts . builder ( ) //構建jwt字串
. setHeaderParam ( "typ" , "JWT" )
. setHeaderParam ( "alg" , "HS256" ) //設定jwt頭資訊
. setSubject ( "guli-user" ) //分類,名字隨便起的,不同的分類可以設定不同的過期
. setIssuedAt ( new Date ( ) ) //設定過期時間的計時起始值為當前時間
. setExpiration ( new Date ( System . currentTimeMillis ( ) + EXPIRE) ) //設定過期時間為當前時間+EXPIRE我們設定的過期時間
. claim ( "id" , id) //token主體,這里放你需要的資訊,我們實作登陸,就放用戶登陸資訊
. claim ( "nickname" , nickname) //需要多少主體資訊,就設定多少個claim屬性
. signWith ( SignatureAlgorithm . HS256, APP_SECRET) //簽名哈希,根據指定規則和我們的密鑰設定簽名
. compact ( ) ;
return JwtToken ;
}
/**
* 根據spring security 物件生成token
* @param userDetails UserDetails物件,spring security會將用戶資訊保存在這個物件
* @return
*/
public String getJwtToken ( UserDetails userDetails) {
//分組荷載,也就是主體資訊,我們將用戶名和有效時間封裝到token中
Map < String , Object > claims = new HashMap < > ( ) ;
claims. put ( CLAIM_KEY_USERNAME, userDetails. getUsername ( ) ) ;
claims. put ( CLAIM_KEY_CREATED, new Date ( ) ) ;
String JwtToken = gennerateToken ( claims) ;
return JwtToken ;
}
private String gennerateToken ( Map < String , Object > claims) {
String JwtToken = Jwts . builder ( ) //構建jwt字串
// .setHeaderParam("typ", "JWT")
// .setHeaderParam("alg", "HS256") //設定jwt頭資訊
//
// .setSubject("dd-user") //分類,名字隨便起的,不同的分類可以設定不同的過期
. setIssuedAt ( new Date ( ) ) //設定過期時間的計時起始值為當前時間
. setExpiration ( new Date ( System . currentTimeMillis ( ) + expiration) ) //設定過期時間為當前時間+EXPIRE我們設定的過期時間
. addClaims ( claims)
// .claim(claims) //token主體,這里放你需要的資訊,我們實作登陸,就放用戶登陸資訊
// .claim() //需要多少主體資訊,就設定多少個claim屬性
. signWith ( SignatureAlgorithm . HS512, secret) //簽名哈希,根據指定規則和我們的密鑰設定簽名
. compact ( ) ;
return JwtToken ;
}
/**
* 從token中獲取登錄用戶名
*/
public String getUsernameByToken ( String token) {
String username;
try {
Claims claims = getClaimsFormToken ( token) ; //獲取主體內容
//claims.get(CLAIM_KEY_USERNAME);
username = claims. getSubject ( ) ; //獲取主體值,上面注釋代碼和這個都可以,獲取的是CLAIM_KEY_USERNAME常量封裝的key,對應的值
} catch ( Exception e) {
username = null ;
e. printStackTrace ( ) ;
}
return username;
}
/**
* 從token中獲取荷載
* @param token
* @return
*/
private Claims getClaimsFormToken ( String token) {
Claims claims = null ;
try {
claims = Jwts . parser ( )
. setSigningKey ( secret)
. parseClaimsJws ( token)
. getBody ( ) ; //決議token字串,獲取主體內容
} catch ( Exception e) {
e. printStackTrace ( ) ;
}
return claims;
}
/**
* 從荷載中獲取過期時間
*/
private Date getExpiredDateFromToken ( String token) {
Claims claimsFormToken = getClaimsFormToken ( token) ;
return claimsFormToken. getExpiration ( ) ;
}
/**
* 驗證token是否失效,失效回傳true,沒失效回傳false
*/
private boolean isTokenExpired ( String token) {
Date expiredDateFromToken = getExpiredDateFromToken ( token) ;
// boolean before = expiredDateFromToken.before(new Date());
return expiredDateFromToken. before ( new Date ( ) ) ; //如果當前時間再過期時間之前,就是還沒有到過期時間,回傳true,否則回傳false
}
/**
* 判斷token是否有效
* @param jwtToken token字串
* @return
*/
public boolean checkToken ( String jwtToken, UserDetails userDetails) {
if ( StringUtils . isEmpty ( jwtToken) ) return false ; //如果為空直接回傳false表示失效
String username;
try {
username = getUsernameByToken ( jwtToken) ; //根據token獲取用戶名
} catch ( Exception e) {
e. printStackTrace ( ) ;
username = null ;
return false ; //獲取不到用戶名,直接失效
}
if ( ! username. equals ( userDetails. getUsername ( ) ) ) return false ; //如果token的用戶名與當前UserDetails用戶名不一致,直接失效
if ( isTokenExpired ( jwtToken) ) return false ; //如果token失效,回傳false
return true ;
}
/**
* 判斷token是否可以被重繪
*/
public boolean canRefresh ( String token) {
return ! isTokenExpired ( token) ; //已經過期,表示可以被重繪
}
/**
* 重繪token
*/
public String refreshToken ( String token) {
Claims claimsFormToken = getClaimsFormToken ( token) ;
claimsFormToken. put ( CLAIM_KEY_CREATED, new Date ( ) ) ;
return gennerateToken ( claimsFormToken) ;
}
}
3. 配置JWT過濾器
任何請求都需要進行過濾,如果請求攜帶我們規定的token,那么將token放在spring Security物件中
import com. dd. security. utils. JwtTokenUtil ;
import org. springframework. beans. factory. annotation. Autowired ;
import org. springframework. beans. factory. annotation. Value ;
import org. springframework. security. authentication. UsernamePasswordAuthenticationToken ;
import org. springframework. security. core. context. SecurityContextHolder ;
import org. springframework. security. core. userdetails. UserDetails ;
import org. springframework. security. core. userdetails. UserDetailsService ;
import org. springframework. security. web. authentication. WebAuthenticationDetailsSource ;
import org. springframework. web. filter. OncePerRequestFilter ;
import javax. servlet. FilterChain ;
import javax. servlet. ServletException ;
import javax. servlet. http. HttpServletRequest ;
import javax. servlet. http. HttpServletResponse ;
import java. io. IOException ;
import java. util. Enumeration ;
/**
* JWT登錄授權過濾器,繼承OncePerRequestFilter
*/
public class JwtAuthencationTokenFilter extends OncePerRequestFilter {
@Value ( "${jwt.tokenHeader}" )
private String tokenHeader; //JWT存盤的請求頭
@Value ( "${jwt.tokenHead}" )
private String tokenHead; //JWT負載中拿到的開頭,token字串,tokenHead是字串的頭,也就是以它開頭
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
/**
* 前置攔截
*/
@Override
protected void doFilterInternal ( HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException , IOException {
// Enumeration<String> headerNames = request.getHeaderNames();//debug用的
//通過tokenHeader獲取驗證頭,也就是tokenHead+token字串
String authHeader = request. getHeader ( tokenHeader) ;
//如果驗證頭存在,并且token字串符合我們規定的tokenHead開頭
if ( authHeader!= null && authHeader. startsWith ( tokenHead) ) {
//將token截取出來
String authToken = authHeader. substring ( tokenHead. length ( ) ) ;
//根據token獲取用戶名
String username = jwtTokenUtil. getUsernameByToken ( authToken) ;
//如果token存在,但是Security全域背景關系沒有用戶,表示用戶沒有登錄
if ( username != null && SecurityContextHolder . getContext ( ) . getAuthentication ( ) == null ) {
//登錄
UserDetails userDetails = userDetailsService. loadUserByUsername ( username) ;
//如果token有效
if ( jwtTokenUtil. checkToken ( authToken, userDetails) ) {
//更新登錄用戶物件UserDetails
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken ( userDetails, null , userDetails. getAuthorities ( ) ) ;
//重新設定到用戶物件中
authenticationToken. setDetails ( new WebAuthenticationDetailsSource ( ) . buildDetails ( request) ) ;
//設定到security全域背景關系
SecurityContextHolder . getContext ( ) . setAuthentication ( authenticationToken) ;
}
}
}
filterChain. doFilter ( request, response) ; //放行
}
}
4. 自定義回傳結果
import com. dd. common_utils. Result ;
import com. fasterxml. jackson. databind. ObjectMapper ;
import org. springframework. security. access. AccessDeniedException ;
import org. springframework. security. web. access. AccessDeniedHandler ;
import org. springframework. stereotype. Component ;
import javax. servlet. ServletException ;
import javax. servlet. http. HttpServletRequest ;
import javax. servlet. http. HttpServletResponse ;
import java. io. IOException ;
import java. io. PrintWriter ;
/**
* 當訪問介面沒有權限,自定義回傳結果
*/
@Component
public class RestAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle ( HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException , ServletException {
response. setCharacterEncoding ( "UTF-8" ) ; //設定回應編碼
response. setContentType ( "application/json" ) ; //設定回應格式,前后端分離最好是json
PrintWriter out = response. getWriter ( ) ; //獲取response輸出流
//創建回傳結果物件
Result result = Result . error ( ) . code ( 403 ) . message ( "權限不足,請聯系管理員" ) ;
//以json字串形式放到response輸出流中
out. write ( new ObjectMapper ( ) . writeValueAsString ( result) ) ;
out. flush ( ) ; //重繪
out. close ( ) ; //關閉流
}
}
當未登錄或token失效時訪問介面,自定義回傳結果
import com. dd. common_utils. Result ;
import com. fasterxml. jackson. databind. ObjectMapper ;
import org. springframework. security. core. AuthenticationException ;
import org. springframework. security. web. AuthenticationEntryPoint ;
import org. springframework. stereotype. Component ;
import javax. servlet. ServletException ;
import javax. servlet. http. HttpServletRequest ;
import javax. servlet. http. HttpServletResponse ;
import java. io. IOException ;
import java. io. PrintWriter ;
/**
* 當未登錄或token失效時訪問介面,自定義回傳結果
*/
@Component
public class RestAuthorizationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence ( HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException , ServletException {
response. setCharacterEncoding ( "UTF-8" ) ; //設定回應編碼
response. setContentType ( "application/json" ) ; //設定回應格式,前后端分離最好是json
PrintWriter out = response. getWriter ( ) ; //獲取response輸出流
//創建回傳結果物件
Result result = Result . error ( ) . code ( 401 ) . message ( "請登錄!!!" ) ;
//以json字串形式放到response輸出流中
out. write ( new ObjectMapper ( ) . writeValueAsString ( result) ) ;
out. flush ( ) ; //重繪
out. close ( ) ; //關閉流
}
}
5. Security配置類
import com. dd. security. entity. DdUser ;
import com. dd. security. exception. RestAccessDeniedHandler ;
import com. dd. security. exception. RestAuthorizationEntryPoint ;
import com. dd. security. filter. JwtAuthencationTokenFilter ;
import com. dd. security. service. DdUserService ;
import org. springframework. beans. factory. annotation. Autowired ;
import org. springframework. context. annotation. Bean ;
import org. springframework. context. annotation. Configuration ;
import org. springframework. security. config. annotation. authentication. builders. AuthenticationManagerBuilder ;
import org. springframework. security. config. annotation. web. builders. HttpSecurity ;
import org. springframework. security. config. annotation. web. builders. WebSecurity ;
import org. springframework. security. config. annotation. web. configuration. WebSecurityConfigurerAdapter ;
import org. springframework. security. config. http. SessionCreationPolicy ;
import org. springframework. security. core. userdetails. UserDetailsService ;
import org. springframework. security. crypto. bcrypt. BCryptPasswordEncoder ;
import org. springframework. security. crypto. password. PasswordEncoder ;
import org. springframework. security. web. authentication. UsernamePasswordAuthenticationFilter ;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DdUserService ddUserService;
@Autowired
private RestAccessDeniedHandler restAccessDeniedHandler; //當訪問介面沒有權限,自定義回傳結果
@Autowired
private RestAuthorizationEntryPoint restAuthorizationEntryPoint; //當未登錄或token失效時訪問介面,自定義回傳結果
/**
* 配置密碼決議Bean實體
* 加載到IOC容器
* 這是Spring Security 自定義登錄邏輯的硬性要求
*/
@Bean
public PasswordEncoder passwordEncoder ( ) {
return new BCryptPasswordEncoder ( ) ;
}
/**
* 配置UserDetailsService
*/
@Bean
@Override
public UserDetailsService userDetailsService ( ) {
return username-> {
DdUser user = ddUserService. getLoginInfoByUsername ( username) ;
if ( user == null ) {
return null ;
}
return user;
} ;
}
/**
* 配置JWT登錄授權過濾器
*/
@Bean
public JwtAuthencationTokenFilter jwtAuthencationTokenFilter ( ) {
return new JwtAuthencationTokenFilter ( ) ;
}
@Override
protected void configure ( AuthenticationManagerBuilder auth) throws Exception {
auth. userDetailsService ( userDetailsService ( ) ) //設定Security使用的userDetailsService,為我們上面配置的
. passwordEncoder ( passwordEncoder ( ) ) ; //設定使用的passwordEncoder為我們上面配置的passwordEncoder
}
@Override
protected void configure ( HttpSecurity http) throws Exception {
//使用JWT不需要csrg
http. csrf ( ) . disable ( )
//使用Token,不需要session
. sessionManagement ( )
. sessionCreationPolicy ( SessionCreationPolicy . STATELESS)
. and ( ) //接下來配置授權
. authorizeRequests ( )
//下面授權在configure(WebSecurity web)方法中配置了,這里不需要了
// .antMatchers("/login","/logout").permitAll()//允許訪問/login,/logout的請求無需認證即可通行
. anyRequest ( ) . authenticated ( ) //除了上面配置的,剩下的請求全部攔截,必須認證通過才能訪問
. and ( ) //接下來配置快取
. headers ( )
. cacheControl ( )
;
//添加JWT登錄授權過濾攔截器
http. addFilterBefore ( jwtAuthencationTokenFilter ( ) , UsernamePasswordAuthenticationFilter . class ) ;
//添加自定義未授權,和未登錄結果回傳,前后端分離,需要回傳狀態碼
http. exceptionHandling ( )
. accessDeniedHandler ( restAccessDeniedHandler)
. authenticationEntryPoint ( restAuthorizationEntryPoint) ;
}
/**
* 放行路徑配置
* @param web
* @throws Exception
*/
@Override
public void configure ( WebSecurity web) throws Exception {
web. ignoring ( )
. antMatchers ( "/login" ,
"/logout" ,
"/css/**" ,
"/js/**" ,
"/swagger-ui.html" ,
"/webjars/**" ,
"/swagger-resources/**" ,
"/v2/api-docs/**"
) ;
}
}
三、實作登錄邏輯
1. 專門給登錄用戶的物體類
2. controller
import com. dd. common_utils. Result ;
import com. dd. security. entity. DdUser ;
import com. dd. security. entity. UserLogin ;
import com. dd. security. service. DdUserService ;
import io. swagger. annotations. Api ;
import io. swagger. annotations. ApiOperation ;
import io. swagger. annotations. ApiParam ;
import org. springframework. beans. factory. annotation. Autowired ;
import org. springframework. web. bind. annotation. GetMapping ;
import org. springframework. web. bind. annotation. PostMapping ;
import org. springframework. web. bind. annotation. RequestBody ;
import org. springframework. web. bind. annotation. RestController ;
import javax. servlet. http. HttpServletRequest ;
import java. security. Principal ;
@RestController
@Api ( tags = "LoginController,登錄" )
public class LoginController {
@Autowired
private DdUserService ddUserService;
@ApiOperation ( value = "登錄之后回傳token,如果想要帶著token請求,需要添加Bearer前綴,Bearer token,中間空格分隔" )
@PostMapping ( "/login" )
public Result login ( @RequestBody UserLogin userLogin, HttpServletRequest request) {
if ( userLogin == null ) {
Result . error ( ) . message ( "請輸入用戶名和密碼" ) ;
}
if ( userLogin. getUsername ( ) == null ) {
Result . error ( ) . message ( "請輸入用戶名" ) ;
}
if ( userLogin. getPassword ( ) == null ) {
Result . error ( ) . message ( "請輸入密碼" ) ;
}
return ddUserService. login ( userLogin, request) ;
}
@ApiOperation ( value = "獲取當前登錄用戶資訊" )
@GetMapping ( "/login/info" )
public Result loginInfo ( @ApiParam ( value = "全域物件,security將資訊設定到全域,通過這個就可以獲取" )
Principal principal) {
if ( principal == null ) {
return Result . error ( ) . message ( "用戶資訊不存在" ) ;
}
String username = principal. getName ( ) ; //從全域中獲取username
//根據用戶名獲取用戶物件
DdUser ddUser = ddUserService. getLoginInfoByUsername ( username) ;
if ( ddUser == null ) {
return Result . error ( ) . message ( "用戶資訊不存在" ) ;
}
ddUser. setPassword ( null ) ; //無論如何都不可以回傳密碼
return Result . ok ( ) . message ( "用戶資訊獲取成功" ) . data ( "loginInfo" , ddUser) ;
}
@ApiOperation ( value = "退出登錄" )
@PostMapping ( "/logout" )
public Result logout ( ) {
//這里省略了退出登錄邏輯,比如銷毀token,銷毀資料庫,redis中快取等,因為沒有引入redis,所以直接回傳退出成功
return Result . ok ( ) . message ( "退出登錄" ) ;
}
}
3. service
import com. baomidou. mybatisplus. core. conditions. query. QueryWrapper ;
import com. dd. common_utils. Result ;
import com. dd. security. entity. DdUser ;
import com. dd. security. entity. UserLogin ;
import com. dd. security. mapper. DdUserMapper ;
import com. dd. security. service. DdUserService ;
import com. baomidou. mybatisplus. extension. service. impl. ServiceImpl ;
import com. dd. security. utils. JwtTokenUtil ;
import org. springframework. beans. factory. annotation. Autowired ;
import org. springframework. beans. factory. annotation. Value ;
import org. springframework. security. authentication. UsernamePasswordAuthenticationToken ;
import org. springframework. security. core. context. SecurityContextHolder ;
import org. springframework. security. core. userdetails. UserDetails ;
import org. springframework. security. core. userdetails. UserDetailsService ;
import org. springframework. security. crypto. password. PasswordEncoder ;
import org. springframework. stereotype. Service ;
import javax. servlet. http. HttpServletRequest ;
@Service
public class DdUserServiceImpl extends ServiceImpl < DdUserMapper , DdUser > implements DdUserService {
@Autowired
private DdUserMapper ddUserMapper;
@Autowired
private UserDetailsService userDetailsService; //spring security UserDetailsService,需要我們自己實作它
@Autowired
private PasswordEncoder passwordEncoder; //PasswordEncoder,需要我們自己配置出來
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value ( "${jwt.tokenHead}" )
private String tokenHead;
/**
* 登錄之后回傳token
*/
@Override
public Result login ( UserLogin userLogin, HttpServletRequest request) {
//獲取userDetails物件,這里我們userDetailsServiceImpl實作的是,它通過用戶名查詢了資料庫的密碼
UserDetails userDetails = userDetailsService. loadUserByUsername ( userLogin. getUsername ( ) ) ;
//如果用戶為空,
if ( userDetails== null ) {
return Result . error ( ) . message ( "用戶不存在" ) ;
}
//密碼匹配失敗
if ( ! passwordEncoder. matches ( userLogin. getPassword ( ) , userDetails. getPassword ( ) ) ) {
return Result . error ( ) . message ( "密碼錯誤" ) ;
}
//下面這個因為我們DdUser將isEnabled寫死為true,所以就不寫這些判斷了,其實都應該寫成屬性,存盤到資料庫
// if(!userDetails.isEnabled()){
// return Result.error().message("用戶被禁言,請聯系管理員");
// }
//更新登錄用戶物件UserDetails 如果獲取物件成功,沒有錯誤,那么使用Spring Security我們需要將物件設定到UserDetails中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken ( userDetails, null , userDetails. getAuthorities ( ) ) ;
SecurityContextHolder . getContext ( ) . setAuthentication ( authenticationToken) ; //將其放在Security全域中
//生成token,回傳
String jwtToken = jwtTokenUtil. getJwtToken ( userDetails) ;
return Result . ok ( ) . message ( "登錄成功,獲取token" )
. data ( "token" , jwtToken) //token字串
. data ( "tokenHead" , tokenHead) ; //tokenheader,key
}
/**
* 根據用戶名獲取登錄物件
* @param username
* @return
*/
@Override
public DdUser getLoginInfoByUsername ( String username) {
DdUser ddUser = ddUserMapper. selectOne ( new QueryWrapper < DdUser > ( ) . eq ( "username" , username) ) ;
if ( ddUser == null ) {
return null ;
}
return ddUser;
}
}
四、通過swagger測驗
1. 配置swagger,攜帶指定請求頭authorization,規定哪些路徑需要認證(只有規定的路徑才會攜帶請求頭)
package com. dd. service_base. config ;
import com. google. common. base. Predicates ;
import org. springframework. context. annotation. Bean ;
import org. springframework. context. annotation. Configuration ;
import springfox. documentation. builders. ApiInfoBuilder ;
import springfox. documentation. builders. PathSelectors ;
import springfox. documentation. service. * ;
import springfox. documentation. spi. DocumentationType ;
import springfox. documentation. spi. service. contexts. SecurityContext ;
import springfox. documentation. spring. web. plugins. Docket ;
import springfox. documentation. swagger2. annotations. EnableSwagger2 ;
import java. lang. reflect. Parameter ;
import java. util. ArrayList ;
import java. util. List ;
@Configuration
@EnableSwagger2 //表中此類為Swagger2
public class SwaggerConfig {
@Bean
public Docket webApiConfig ( ) {
List < Parameter > pars = new ArrayList < Parameter > ( ) ;
return new Docket ( DocumentationType . SWAGGER_2)
. groupName ( "webApi" ) //分組名
. apiInfo ( webApiInfo ( ) ) //在線檔案的資訊,傳入ApiInfo物件,就是下面內個方法回傳的物件
. select ( )
. paths ( Predicates . not ( PathSelectors . regex ( "/admin/.*" ) ) ) //路徑中包含admin時不顯示資訊
. paths ( Predicates . not ( PathSelectors . regex ( "/error/.*" ) ) )
. build ( )
//因為我們開啟了securityJWT驗證,所以我們也需要給Swagger配置頭資訊
. securityContexts ( securityContexts ( ) )
. securitySchemes ( securitySchemes ( ) )
;
}
private ApiInfo webApiInfo ( ) {
return new ApiInfoBuilder ( )
. title ( "gulischool 介面 API 檔案" )
. description ( "展示先做基礎功能,后面再添加業務" )
. termsOfServiceUrl ( "https://www.dd.com/aa/" )
. version ( "1.0" )
. contact ( new Contact ( "Helen" , "http://dd.com" , "915501928@qq.com" ) )
. build ( ) ;
}
private List < ApiKey > securitySchemes ( ) {
//設定頭資訊
ArrayList < ApiKey > result = new ArrayList < > ( ) ;
ApiKey apiKey = new ApiKey ( "Authorization" , "Authorization" , "Header" ) ;
result. add ( apiKey) ;
return result;
}
/**
* 設定哪些路徑需要授權
* @return
*/
private List < SecurityContext > securityContexts ( ) {
//設定需要授權認證的路徑
ArrayList < SecurityContext > result = new ArrayList < > ( ) ;
result. add ( getContextByPath ( "/service_animation/.*" ) ) ;
// result.add(getContextByPath("/login/.*"));
result. add ( getContextByPath ( "/login/info" ) ) ;
result. add ( getContextByPath ( "/logout" ) ) ;
result. add ( getContextByPath ( "/security/*" ) ) ;
return result;
}
private SecurityContext getContextByPath ( String pathRegex) {
return SecurityContext . builder ( )
. securityReferences ( defaultAuth ( ) )
// .forPaths(PathSelectors.regex(pathRegex))
. forPaths ( PathSelectors . regex ( "^(?!auth).*$" ) )
. build ( ) ;
}
private List < SecurityReference > defaultAuth ( ) {
ArrayList < SecurityReference > result = new ArrayList < > ( ) ;
//設定授權范圍為全域global
AuthorizationScope authorizationScope = new AuthorizationScope ( "global" , "accessEverything" ) ;
//官方要求必須放在陣列中,所以將其放在陣列中
AuthorizationScope [ ] authorizationScopes = new AuthorizationScope [ 1 ] ;
authorizationScopes[ 0 ] = authorizationScope;
//官方要求List中添加new SecurityReference("Authorization",authorizationScopes)
result. add ( new SecurityReference ( "Authorization" , authorizationScopes) ) ;
return result;
}
}
2. 測驗
前端呢,只需要登錄后按規則保存token,請求時,攜帶這個token就可以訪問了
五、驗證碼實作
1. 生成驗證碼
import com. google. code. kaptcha. impl. DefaultKaptcha ;
import com. google. code. kaptcha. util. Config ;
import org. springframework. context. annotation. Bean ;
import org. springframework. context. annotation. Configuration ;
import java. util. Properties ;
/**
* 驗證碼配置類
*/
@Configuration
public class CaptchaConfig {
@Bean
public DefaultKaptcha defaultKaptcha ( ) {
//驗證碼生成器
DefaultKaptcha defaultKaptcha = new DefaultKaptcha ( ) ;
//配置
Properties properties = new Properties ( ) ;
//時候有邊框
properties. setProperty ( "kaptcha.border" , "yes" ) ;
//設定邊框顏色
properties. setProperty ( "kaptcha.border.color" , "105,179,98" ) ;
//驗證碼
properties. setProperty ( "kaptcha.session.key" , "code" ) ;
//驗證碼文本字符顏色,默認黑
properties. setProperty ( "kaptcha.textproducer.font.color" , "blue" ) ;
//設定字體樣式
properties. setProperty ( "kaptcha.textproducer.font.names" , "宋體,楷體,微軟雅黑" ) ;
//設定字體大小默認40
properties. setProperty ( "kaptcha.textproducer.font.size" , "30" ) ;
//驗證碼文本字符內容范圍 默認abced2345678gfynmnpwx
// properties.setProperty("kaptcha.textproducer.char.string","");
//字符長度,默認5
properties. setProperty ( "kaptcha.textproducer.char.length" , "4" ) ;
//字符間距,默認2
properties. setProperty ( "kaptcha,textproducer.char.space" , "4" ) ;
//驗證碼圖片寬度,默認208
properties. setProperty ( "kaptcha.image.width" , "100" ) ;
//驗證碼圖片高度,默認40
properties. setProperty ( "kaptcha.image.height" , "40" ) ;
Config config = new Config ( properties) ;
defaultKaptcha. setConfig ( config) ;
return defaultKaptcha;
}
}
import com. google. code. kaptcha. impl. DefaultKaptcha ;
import io. swagger. annotations. ApiOperation ;
import org. springframework. beans. factory. annotation. Autowired ;
import org. springframework. web. bind. annotation. GetMapping ;
import org. springframework. web. bind. annotation. RestController ;
import javax. imageio. ImageIO ;
import javax. servlet. ServletOutputStream ;
import javax. servlet. http. HttpServletRequest ;
import javax. servlet. http. HttpServletResponse ;
import java. awt. image. BufferedImage ;
import java. io. IOException ;
@RestController
public class CaptchaController {
@Autowired
private DefaultKaptcha defaultKaptcha;
@ApiOperation ( value = "驗證碼,以image/jpeg格式固定回應" )
@GetMapping ( value = "/captcha" , produces = "image/jpeg" )
public void captcha ( HttpServletRequest request, HttpServletResponse response) {
/**定義response輸出型別為image/jpeg型別**/
response. setDateHeader ( "Expires" , 0 ) ;
// Set standard HTTP/1.1 no-cache headers.
response. setHeader ( "Cache-Control" , "no-store, no-cache, must-revalidate" ) ;
// Set IE extended HTTP/1.1 no-cache headers(use addHeader)
response. addHeader ( "Cache-Control" , "post-check=0,pre-check=0" ) ;
// Set standard HTTP/1.0 no-cache header.
response. setHeader ( "Pragma" , "no-cache" ) ;
// return a jpeg
response. setContentType ( "image/jpeg" ) ;
/**生成驗證碼 begin**/
String text = defaultKaptcha. createText ( ) ;
System . out. println ( "驗證碼:" + text) ;
//將驗證碼文本內容放入session
request. getSession ( ) . setAttribute ( "captcha" , text) ;
//根據文本驗證碼內容創建圖形驗證碼
BufferedImage image = defaultKaptcha. createImage ( text) ;
ServletOutputStream outputStream = null ;
try {
outputStream = response. getOutputStream ( ) ;
//輸出流輸出.jpg格式圖片
ImageIO . write ( image, "jpg" , outputStream) ;
outputStream. flush ( ) ;
} catch ( IOException e) {
e. printStackTrace ( ) ;
} finally {
if ( outputStream!= null ) {
try {
outputStream. close ( ) ;
} catch ( IOException e) {
e. printStackTrace ( ) ;
}
}
}
}
}
2. 修改service邏輯
//從session中獲取驗證碼
String captcha = ( String ) request. getSession ( ) . getAttribute ( "captcha" ) ;
//如果驗證碼為空,或者驗證碼和session中驗證碼不一致(忽略大小寫比較),回傳錯誤
if ( StringUtils . isEmpty ( captcha) || ! captcha. equalsIgnoreCase ( userLogin. getCode ( ) ) ) {
return Result . error ( ) . message ( "驗證碼錯誤!!!" ) ;
}
六、實作權限管理系統
因為篇幅限制,我將其放在這篇文章中https://blog.csdn.net/grd_java/article/details/121932440