多语言展示
当前在线:1835今日阅读:23今日分享:25

springboot项目(9)集成security

Spring Security是一个功能强大且可高度自定义的身份验证和访问控制框架。它是保护基于Spring的应用程序的事实标准。相较于shiro,Spring Security功能更全,与spring框架更能做到无缝对接。
工具/原料
1

电脑

2

intelliJ idea,jdk1.8,mysql

方法/步骤
1

集成spring security首先需要引入依赖:     org.springframework.boot     spring-boot-starter-security      io.jsonwebtoken     jjwt     0.9.1      org.json     json     20180813 

2

security主要做两件事情:第一件事是用户验证,第二件事是判断权限。那么如何做着两件事情呢?接着往下看,首先我们需要配置一个配置类,这个配置类主要是做HTTP 验证和自定义身份验证。配置类如下:package cn.cy.config;  import cn.cy.core.filter.JWTAuthenticationFilter; import cn.cy.core.filter.JWTLoginFilter; import cn.cy.core.handler.JWTAuthenctiationSuccessHandler; import cn.cy.service.SysUserDedailsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;  /**  * @EnableWebSecurity 主要是引入WebSecurityConfiguration.class和SpringWebMvcImportSelector.class,  * Spring Security默认是禁用注解的,要想开启注解,  * 需要在继承WebSecurityConfigurerAdapter的类上加@EnableGlobalMethodSecurity注解,  * 来判断用户对某个控制层的方法是否具有访问权限  *  * @EnableGlobalMethodSecurity(securedEnabled=true) 开启@Secured 注解过滤权限  * @EnableGlobalMethodSecurity(jsr250Enabled=true)开启@RolesAllowed 注解过滤权限  * @EnableGlobalMethodSecurity(prePostEnabled=true) 使用表达式时间方法级别的安全性  *  * @PreAuthorize 在方法调用之前,基于表达式的计算结果来限制对方法的访问  * @PostAuthorize 允许方法调用,但是如果表达式计算结果为false,将抛出一个安全性异常  * @PostFilter 允许方法调用,但必须按照表达式来过滤方法的结果  * @PreFilter 允许方法调用,但必须在进入方法之前过滤输入值  *  * Created by 30721 on 2019/7/10.  */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled =true) public class SecurityConfig extends WebSecurityConfigurerAdapter {      @Autowired     private SysUserDedailsService sysUserDedailsService;     @Autowired     private JWTAuthenctiationSuccessHandler jwtAuthenctiationSuccessHandler;      /**      * 可以根据现实预期,编写自定义加密方法      * @return      */     @Bean     public PasswordEncoder bCryptPasswordEncoder() {         return new BCryptPasswordEncoder();     }      /**      * 设置 HTTP 验证规则      * 默认配置为:      * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();      * 退出登录的地址为 '/logout',退出成功后跳转到页面 '/login'      * 默认启用 CSRF      * @param http      * @throws Exception      */     @Override     protected void configure(HttpSecurity http) throws Exception {         //开启跨域 关闭csrf。         http.cors().and().csrf().disable()                 .authorizeRequests()                 //开启swagger2的必要权限                 .antMatchers('/v2/api-docs', '/configuration/ui', '/swagger-resources', '/configuration/security', '/swagger-ui.html', '/webjars/**','/swagger-resources/configuration/ui').permitAll()                 //开启api的匿名访问                 .antMatchers('/api/**').permitAll()                 //角色为管理员的时候才有权限访问admin地址                 .antMatchers('/admin/**').access('hasRole('ADMIN')')                 //角色为USER的时候才有权限访问user地址                 .antMatchers('/sysUsers/**').access('hasRole('USER')')                 //由于该项目定位是前后端分离,这个项目只涉及到后端,所以没有HTML css等文件,所以注释掉,需要的就将他打开                 //.antMatchers('/', '/*.html','/favicon.ico', '/css/**', '/js/**').permitAll()                 .anyRequest().authenticated()                 .and()                 .formLogin() //                自定义登入页 //                .loginPage('/login') //                自定义登入路径 //                .loginProcessingUrl('') //                认证成功后的跳转路径 //                .successForwardUrl('/home') //                授权成功处理程序  --- 但是我试了一下却不能正常跳转到处理器的方法里面,暂时还没找到原因 //                .successHandler(jwtAuthenctiationSuccessHandler)                 .permitAll()                 .and()                 //注销                 .logout()                 .permitAll();         //拦截器重写         http.addFilter(new JWTLoginFilter(authenticationManager()));         //授权验证拦截重写         http.addFilterBefore(new JWTAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);     }      /**      * 使用自定义身份验证组件      * 重写安全认证,加入密码加密方式      * @param auth      * @throws Exception      */     @Override     public void configure(AuthenticationManagerBuilder auth) throws Exception {         auth.userDetailsService(sysUserDedailsService).passwordEncoder(bCryptPasswordEncoder());     } }

3

写完配置文件,接下来就是编写登录的拦截验证和授权了:package cn.cy.core.filter;  import cn.cy.model.ResultBean; import cn.cy.util.JWTUtil; import cn.cy.util.ResponseUtil; import com.google.gson.JsonIOException; import com.google.gson.JsonSyntaxException; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;  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.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map;  /**  * 验证用户名密码正确后,生成一个token,并将token返回给客户端  * 该类继承自UsernamePasswordAuthenticationFilter,重写了其中的2个方法  * attemptAuthentication :接收并解析用户凭证。  * successfulAuthentication :用户成功登录后,这个方法会被调用,我们在这个方法里生成token。  * Created by 30721 on 2019/7/10.  */ public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {      private AuthenticationManager authenticationManager;      public JWTLoginFilter(AuthenticationManager authenticationManager) {         this.authenticationManager = authenticationManager;     }      /**      * 解析用户并判断其合法性。      * @param request      * @param response      * @return      * @throws AuthenticationException      */     @Override     public Authentication attemptAuthentication(HttpServletRequest request,                                                 HttpServletResponse response) throws AuthenticationException {         try { //            自定义的页面--获取user信息 //            SysUser user = GsonUtil.getInstance().fromJson(request.getReader(),SysUser.class); //            return authenticationManager.authenticate( //                    new UsernamePasswordAuthenticationToken( //                            user.getName(), //                            user.getPsw(), //                            new ArrayList<>()));             return authenticationManager.authenticate(                     new UsernamePasswordAuthenticationToken(                             request.getParameter('username'),                             request.getParameter('password'),                             new ArrayList<>()));         } catch (JsonSyntaxException | JsonIOException |  NullPointerException e) {             e.printStackTrace();             ResultBean result = new ResultBean<>();             result.setState(1);             result.setMsg('参数格式错误');             ResponseUtil.responseJson(response, HttpStatus.BAD_REQUEST.value(), result);         }         return null;     }      @Override     protected void successfulAuthentication(HttpServletRequest request,                                             HttpServletResponse response,                                             FilterChain chain,                                             Authentication auth) throws IOException, ServletException {         UserDetails userDetails = (UserDetails) auth.getPrincipal();         Map map = new HashMap<>();         map.put('name',userDetails.getUsername());         List list = new ArrayList<>();         for (GrantedAuthority grantedAuthority: userDetails.getAuthorities()) {             list.add(grantedAuthority.getAuthority());         }         map.put('auth', list);         //生成token         String tokenStr = JWTUtil.createJWT(map,0L);         //header中添加 token         response.addHeader('Authorization', 'Bearer ' + tokenStr);         ResultBean result = new ResultBean<>();         result.setState(0);         result.setMsg('认证成功');         result.setResultBean(tokenStr);         ResponseUtil.responseJson(response, HttpStatus.OK.value(), result);     }      /**      * y      * @param request      * @param response      * @param failed      * @throws IOException      * @throws ServletException      */     @Override     protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {         String msg;         if (failed instanceof BadCredentialsException) {             msg = '密码错误';         } else {             msg = failed.getMessage();         }         ResultBean result = new ResultBean<>();         result.setState(1);         result.setMsg(msg);         ResponseUtil.responseJson(response, HttpStatus.UNAUTHORIZED.value(), result);     } }

4

验证用户的接口(接口中没有写密码验证,是因为springboot帮助我们做了密码验证,但是需要在配置类中告诉springboot你用的是什么加密方式,官方推荐的是bCryptPasswordEncoder,我这边用的也是这个,这个加密 方式是可以自己定义的,这边不做描述)代码如下:/**  * Created by 30721 on 2019/7/11.  */ @Service public class SysUserDedailsService implements UserDetailsService {      @Autowired     private SysUserService sysUserService;      /**      * 查询用户是否存在,并授予角色      * 这边不做密码校验,密码校验交给springboot。      */      @Override     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {         SysUser sysUser = sysUserService.getSysUser(username);         if (null == sysUser) {             throw new UsernameNotFoundException(username);         }         List authorities = new ArrayList<>();         //这里简单的做一下,权限赋予,业务需求需要根据实际情况。         if (username.equals('admin')) {             authorities.add(new SimpleGrantedAuthority('ADMIN'));             authorities.add(new SimpleGrantedAuthority('USER'));         } else {             authorities.add(new SimpleGrantedAuthority('USER'));         }         return new User(sysUser.getName(), sysUser.getPsw(), authorities);     } }

5

由于这边用的是token方式记录用户信息与权限,服务器没有记录用户状态,所以服务器算是无状态服务器,无状态服务器的好处体现在分布式系统中这里也不多赘述。系统使用token方式而服务器没有记录用户的状态,所以服务器需要解析token来判断用户是否为登入状态和用户权限。所以需要编写授权拦截器解析token,代码如下:/**  * Created by 30721 on 2019/7/11.  */ public class JWTAuthenticationFilter extends BasicAuthenticationFilter {      public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {         super(authenticationManager);     }      @Override     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {         String header = request.getHeader('Authorization');         if (header == null || !header.startsWith('Bearer ')) {             chain.doFilter(request, response);             return;         }         UsernamePasswordAuthenticationToken authentication = getAuthentication(request);         SecurityContextHolder.getContext().setAuthentication(authentication);         chain.doFilter(request, response);      }      private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {         String token = request.getHeader('Authorization');         try {             if (token != null) {                 token = token.replace('Bearer ', '');                 //解析token                 Claims claims = JWTUtil.verifyJwt(token);                 if (claims != null) {                     List list = (List) claims.get('auth');                     //解析权限                     List authorities = new ArrayList<>();                     for (String str: list) {                         authorities.add(new SimpleGrantedAuthority(str));                     }                     UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(                             claims.get('name'),                             null,  authorities);                     return authentication;                 }             }         } catch (Exception e) {             e.printStackTrace();             return null;         }         return null;     } }

6

由于运用到token,所以需要编写一个生成和解析token的工具类,代码如下:package cn.cy.util;  import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.apache.tomcat.util.codec.binary.Base64;  import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.util.Date; import java.util.HashMap; import java.util.Map;  /**  * Created by 30721 on 2019/7/12.  */ public class JWTUtil {      /**      * token 过期时间, 单位: 秒. 这个值表示 30 天      */     private static final long TOKEN_EXPIRED_TIME = 30 * 24 * 60 * 60;      /**      * 加密解密密钥      */     private static final String JWT_SECRET = '5d5b09f6dcb2d53a5fffc60c4ac0d55fabdf556069d6631545f42aa6e3500f2e';      public static final String jwtId = 'tokenId';     /**      * 创建JWT      */     public static String createJWT(Map claims, Long time) {         SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;         Date now = new Date(System.currentTimeMillis());          SecretKey secretKey = generalKey();         long nowMillis = System.currentTimeMillis();//生成JWT的时间         JwtBuilder builder = Jwts.builder()                 .setClaims(claims)                 .setId(jwtId)                  //设置JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。                 .setIssuedAt(now)           //iat: jwt的签发时间                 .signWith(signatureAlgorithm, secretKey);//设置签名使用的签名算法和签名使用的秘钥         if (time > 0) {             long expMillis = nowMillis + time;             Date exp = new Date(expMillis);             builder.setExpiration(exp);     //设置过期时间         } else {//否则设置默认时间             long expMillis = nowMillis + TOKEN_EXPIRED_TIME;             Date exp = new Date(expMillis);             builder.setExpiration(exp);     //设置过期时间         }         return builder.compact();     }      /**      * 验证jwt      */     public static Claims verifyJwt(String token) {         //签名秘钥,和生成的签名的秘钥一模一样         SecretKey key = generalKey();         Claims claims;         try {             claims = Jwts.parser()  //得到DefaultJwtParser                     .setSigningKey(key)         //设置签名的秘钥                     .parseClaimsJws(token).getBody();         } catch (Exception e) {             claims = null;         }//设置需要解析的jwt         return claims;      }       /**      * 由字符串生成加密key      *      * @return      */     public static SecretKey generalKey() {         String stringKey = JWT_SECRET;         byte[] encodedKey = Base64.decodeBase64(stringKey);         SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, 'AES');         return key;     }  }

7

由于项目开始的定位就是前后端分离,所以本次的验证,我借助了Fiddler 4的工具来进行的。

8

验证成功后我们试一下能不能访问home页面,因为我们使用的token的方式来进行的,所以我们需要将登入成功后的token拿到,如下图;拿到token后粘贴在请求参数的输出框中,修改路径,点击执行。

推荐信息