• Spring Security 安全认证


    Spring Boot 使用 Mybatis

    依赖
    
    	<dependency>
    	    <groupId>org.mybatis.spring.boot</groupId>
    	    <artifactId>mybatis-spring-boot-starter</artifactId>
    	</dependency>
    
    
    使用:
    
    	启动类上注解:@MapperScan("com.dao"),或者每个 dao 接口上添加注解:@Mapper
    
    	@Mapper 是 MyBatis 的注解,目的是为了让 spring 能够根据 xml 和这个接口动态生成接口的实现。
    
    	@Mapper
    	public interface UserDao {
    	    @Select("select * from user where id=#{id}")
    	    User GetUserbyId(@Param("id") Integer id);
    	}
    
    
    	注解方式:
    
    	@Select 是查询类的注解,所有的查询均使用这个
    	@Result 修饰返回的结果集,关联实体类属性和数据库字段一一对应,如果实体类属性和数据库属性名保持一致,就不需要这个属性来修饰。
    	@Insert 插入数据库使用,直接传入实体类会自动解析属性到对应的值
    	@Update 负责修改,也可以直接传入对象
    	@Delete 负责删除
    
    
    	mapper.xml 方式:
    
    	<?xml version="1.0" encoding="UTF-8"?>
    	<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    	<mapper namespace="com.dao.UserDao">
    
    	    <!-- 可根据自己的需求,是否要使用 -->
    	    <resultMap type="User" id="UserDaoMap">
    	        <id column="id" property="id" jdbcType="INTEGER" />
    	        <result column="userName" property="useName" jdbcType="VARCHAR" />
    	    </resultMap>
    	    
    	    <select id="GetUserbyId" parameterType="INTEGER" resultMap="UserDaoMap">
    	        select *
    	        from user
    	        where 1=1
    	            and id = #{id,jdbcType=INTEGER}
    	    </select>
    
    	</mapper>
    
    	namespace 指定这个 xml 所对应的是哪个 dao 接口。
    
    	select 标签的 id 对应的就是 dao 接口中的方法名,parameterType 就是传进来的参数类型。
    

    Mysql 去掉 ONLY_FULL_GROUP_BY

    ONLY_FULL_GROUP_BY:
    	对于GROUP_BY聚合操作,如果在SELECT中的列既没有在GROUP_BY中出现,
    	本身也不是聚合列(使用SUM,ANG等修饰的列),那么这句SQL是不合法的,因为那一列是不确定的。
    
    SELECT @@global.sql_mode;
    
    SET @@global.sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';
    

    Spring Security oAuth2

    Spring Security 基于 Servlet 过滤器、IoC 和 AOP,
    为 Web 请求和方法调用提供身份确认和授权处理,避免了代码耦合,减少了大量重复代码工作。
    
    oAuth2 是授权标准,协议,体现在代码上是接口。实现有 Spring Security 和 Shire。
    
    
    Access Token 是客户端访问资源服务器的令牌(Access Token 临时的,有一定有效期)。
    
    Refresh Token 的作用是用来刷新 Access Token(Refresh Token 效期非常长)。
    刷新接口类似于:http://www.funtl.com/refresh?refresh_token=&client_id=&client_secret=
    

    认证:Authentication 授权:Authorization

    认证(Authentication):确定一个用户的身份的过程。
    
    授权(Authorization):判断一个用户是否有访问某个安全对象的权限。
    
    认证与授权中关键的三个过滤器:
    
    	1. UsernamePasswordAuthenticationFilter:
    		该过滤器用于拦截我们表单提交的请求(默认为/login),进行用户的认证过程。
    
    	2. ExceptionTranslationFilter:
    		该过滤器主要用来捕获处理 Spring Security 抛出的异常,异常主要来源于 FilterSecurityInterceptor。
    
    	3. FilterSecurityInterceptor:
    		该过滤器主要用来进行授权判断。
    

    Spring Security 核心组件

    Spring Security 核心组件有:
    	SecurityContext
    	SecurityContextHolder
    	Authentication
    	Userdetails
    	AuthenticationManager
    
    
    SecurityContext(安全上下文):
    
    	当用户通过认证之后,就会为这个用户生成一个唯一的SecurityContext,里面包含用户的认证信息Authentication。
    
    	public interface SecurityContext extends Serializable {
       		Authentication getAuthentication();
        	void setAuthentication(Authentication var1);
    	}
    
    
    SecurityContextHolder(安全上下文持有者):
    
    	在SecurityContextHolder中保存的是当前访问者的信息。Spring Security使用一个Authentication对象来表示这个信息。
    
    	缺省情况下,SecurityContextHolder使用了ThreadLocal机制来保存每个使用者的安全上下文。
    
    	SecurityContextHolder可以工作在以下三种模式之一:
    
    		MODE_THREADLOCAL (缺省工作模式)
    		MODE_GLOBAL(所有线程中都相同)
    		MODE_INHERITABLETHREADLOCAL(存储在线程中,但子线程可以获取到父线程中的 SecurityContext)
    
    	修改SecurityContextHolder的工作模式有两种方法:
    
    		设置一个系统属性(system.properties):spring.security.strategy
    
    		调用SecurityContextHolder静态方法:setStrategyName()
    
    	存储当前认证信息:
    		SecurityContextHolder.getContext().setAuthentication(token);
    
    	获取保存在ThreadLocal中的用户信息:
    		SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    
    
    Authentication(认证):
    
    	public interface Authentication extends Principal, Serializable {
    	    Collection<? extends GrantedAuthority> getAuthorities();	//获取用户权限信息
    
    	    Object getCredentials();	// 获取认证信息
    
    	    Object getDetails();	//获取用户描述信息
    
    	    Object getPrincipal();	//获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails
    
    	    boolean isAuthenticated();	//是否已经认证
    
    	    void setAuthenticated(boolean var1) throws IllegalArgumentException;
    	}
    
    
    UserDetails
    
    	public interface UserDetails extends Serializable {
    	    Collection<? extends GrantedAuthority> getAuthorities();
    
    	    String getPassword();
    
    	    String getUsername();
    
    	    boolean isAccountNonExpired();
    
    	    boolean isAccountNonLocked();
    
    	    boolean isCredentialsNonExpired();
    
    	    boolean isEnabled();
    	}
    
    
    UserDetailsService
    
    	UserDetailsService也是一个接口,且只有一个方法loadUserByUsername,用来获取UserDetails。
    
    
    AuthenticationManager
    
    	AuthenticationManager 是一个接口,它只有一个方法,接收参数为Authentication,其定义如下:
    
    	public interface AuthenticationManager {
    		Authentication authenticate(Authentication authentication)
    		throws AuthenticationException;
    	}
    
    	AuthenticationManager 的作用就是校验 Authentication,
    	如果验证失败会抛出 AuthenticationException 异常。
    

    pre-post-annotations

    Spring Security 中定义了四个支持使用表达式的注解,分别是:
    
    	@PreAuthorize
    		@PreAuthorize("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")
    		@PreAuthorize("#user.name.equals('abc')")
    
    	@PostAuthorize
    		@PostAuthorize 是在方法调用完成后进行权限检查,
    		它不能控制方法是否能被调用,只能在方法调用完成后检查权限决定是否要抛出 AccessDeniedException。
    
    	@PreFilter
    		@PreFilter(filterTarget="ids", value="filterObject%2==0"):保留ids集合中的偶数
    
    	@PostFilter
    
    	其中前两者可以用来在方法调用前或者调用后进行权限检查,
    	后两者可以用来对集合类型的参数或者返回值进行过滤。
    

    hasRole 和 hasAuthority

    角色授权:授权代码需要加 ROLE_ 前缀,Controller 上使用时不要加前缀。
    
    权限授权:设置和使用时,名称保持一至即可。
    

    使用流程

    WebSecurityConfig(配置文件)

    @Configuration
    /**
     * @EnableWebSecurity注解有两个作用:
     *      1: 加载了WebSecurityConfiguration, 配置安全策略。
     *      2: 加载了AuthenticationConfiguration, 配置了认证信息。
     */
    @EnableWebSecurity
    /**
     * prePostEnabled = true:
     *      使用表达式时间方法级别的安全性
     *      4个注解可用:@PreAuthorize等
     */
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private UserService userService;
    
        @Bean
        public JwtTokenFilter authenticationTokenFilterBean() throws Exception {
            return new JwtTokenFilter();
        }
    
        /**
         * 用于认证过程中载入用户信息:public class UserService implements UserDetailsService
         */
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());
        }
    
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception {
            /**
             * SpringSecurity会把匹配规则按注册先后顺序放到一个ArrayList<UrlMapping>中,
             * 先注册的规则放前面,后注册的放后面。
             */
            httpSecurity
            		//禁用csrf
                    .csrf().disable()
                    /**
                     * 有多个配置HttpSecurity的配置类时必须设置.antMatcher("/**"),
                     * 因为不设置的话默认匹配所有,就不会执行到下面的HttpSecurity了。
                     *
                     * 需要在类上加注解@Order,指明优先级
                     */
                    .antMatcher("/**")
                    //设置无状态SessionCreationPolicy(不需要Session)
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    //登陆页
                    .formLogin()
                    .loginPage("/login")  //登陆页
                    .failureUrl("/login")   //登陆失败的页面跳转URL
                    .defaultSuccessUrl("/index")  //登录成功后的默认处理页
                    .permitAll()
                    .and()
                    //注销页
                    .logout()
    				.logoutUrl("/logout")	//注销接口
    				.logoutSuccessUrl("/login")	//注销成功跳转接口
    				.permitAll()
    				.and()
                    //通过 authorizeRequests() 定义哪些URL需要被保护、哪些不需要被保护
                    .authorizeRequests()
                    //OPTIONS请求全部放行
                    .antMatchers(HttpMethod.OPTIONS).permitAll()
                    //其他 post put delete get 请求全部拦截校验
                    .antMatchers(HttpMethod.POST).authenticated()    //需要认证
                    .antMatchers(HttpMethod.PUT).authenticated()
                    .antMatchers(HttpMethod.DELETE).authenticated()
                    .antMatchers(HttpMethod.GET).authenticated();
    
            /**
             * addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter)
             * 在 beforeFilter 之前添加 filter。
             * 
             * UsernamePasswordAuthenticationFilter:登陆用户密码验证过滤器
             */
            httpSecurity
                    .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
    
            /**
             * 禁用默认响应安全头,添加要用的响应安全头:
             * 		httpSecurity.headers().defaultsDisabled().cacheControl();
             */
            httpSecurity.headers().cacheControl();
        }
    }
    

    JwtTokenFilter过滤器

    每次请求都经过过滤器,判断是否持有Token(已经登陆),如果持有Token就进行认证,否则以游客身份认证。

    @Component
    public class JwtTokenFilter extends OncePerRequestFilter {
    
        @Autowired
        private UserService userService;
    
        @Autowired
        private JwtConfig jwtConfig;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
            boolean giveFlag = false;
    
            //从请求头中获取token
            String authHeader = request.getHeader(jwtConfig.getHeader());
    
            //认证
            if (authHeader != null && authHeader.startsWith(jwtConfig.getPrefix())) {
                //如果token存在,并且格式正确,就从数据库查询UserDetails
                UserDetails userDetails = userService.loadUserByToken(authHeader);
    
                if (null != userDetails) {
                    if (SecurityContextHolder.getContext().getAuthentication() == null) {
                    	//用户存在,本次会话的权限还未被写入
                        //生成认证信息
                        UsernamePasswordAuthenticationToken authentication =
                                new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                        //将认证信息写入本次会话
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                } else {
                    //用户不存在,以游客身份认证
                    giveFlag = true;
                }
            } else {
                //token不存在或者格式错误,以游客身份认证
                giveFlag = true;
            }
    
            //给游客授权
            if (giveFlag) {
                List<SimpleGrantedAuthority> authorities = new ArrayList<>();
                authorities.add(new SimpleGrantedAuthority("NORMAL"));
                User user = new User("NORMAL", "NORMAL", authorities);
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
    
            chain.doFilter(request, response);
        }
    }
    

    UserService

    @Service
    public class UserService implements UserDetailsService {
    
        @Autowired
        private UserDao userDao;
    
        @Autowired
        private RoleDao roleDao;
    
        @Autowired
        private HttpServletRequest request;
    
        @Autowired
        private BCryptPasswordEncoder encoder;
    
        @Autowired
        private JwtTokenUtil jwtTokenUtil;
    
        @Autowired
        private JwtConfig jwtConfig;
    
        @Autowired
        private RedisTemplate<String, String> redisTemplate;
    
    
        /**
         * 登录(校验User,通过校验则生成Token并放入Redis,并返回Token)
         */
        public String login(User user) {
    
            User dbUser = userDao.findUserByName(user.getName());
    
            //用户名或密码错误
            if (null == dbUser || !encoder.matches(user.getPassword(), dbUser.getPassword())) {
                throw new UsernameNotFoundException("用户名或密码错误");
            }
            //用户已被封禁
            if (0 == dbUser.getState()) {
                throw new RuntimeException("你已被封禁");
            }
    
            //生成Token
            final UserDetails userDetails = this.loadUserByUsername(user.getName());
            final String token = jwtTokenUtil.generateToken(userDetails);
    
            //key:TOKEN_username,value:Bearer token
            redisTemplate.opsForValue().
                    set(JwtConfig.REDIS_TOKEN_KEY_PREFIX + user.getName(), jwtConfig.getPrefix() + token, jwtConfig.getTime(), TimeUnit.SECONDS);
    
            return token;
        }
    
    
        @Override
        public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
            User user = userDao.findUserByName(name);
    
            List<SimpleGrantedAuthority> authorities = new ArrayList<>(1);
            List<Role> roles = roleDao.findUserRoles(user.getId());
            for (Role role : roles) {
                authorities.add(new SimpleGrantedAuthority(role.getName()));
            }
            return new org.springframework.security.core.userdetails.User(user.getName(), null, authorities);
        }
    
    
        /**
         * 从Token中提取信息,校验Token
         */
        public UserDetails loadUserByToken(String authHeader) {
        	//除去前缀,获取Token
            final String authToken = authHeader.substring(jwtConfig.getPrefix().length());
    
            String username = jwtTokenUtil.getUsernameFromToken(authToken);
            
            //Token非法
            if (null == username) {
                return null;
            }
    
            //通过username从缓存中获取Token:判断Token是否过期,或者Token是否匹配
            String redisToken = redisTemplate.opsForValue().get(JwtConfig.REDIS_TOKEN_KEY_PREFIX + username);
            if (!authHeader.equals(redisToken)) {
                return null;
            }
    
            List<String> roles = jwtTokenUtil.getRolesFromToken(authToken);
            List<SimpleGrantedAuthority> authorities = new ArrayList<>();
            for (String role : roles) {
                authorities.add(new SimpleGrantedAuthority(role));
            }
    
            return new org.springframework.security.core.userdetails.User(username, null, authorities);
        }
    
    
        /**
         * 退出登录(清除Redis缓存中的Token)
         */
        public void logout() {
            String username = jwtTokenUtil.getUsernameFromRequest(request);
            redisTemplate.delete(JwtConfig.REDIS_TOKEN_KEY_PREFIX + username);
        }
    }
    

    JwtTokenUtil

    public interface Claims extends Map<String, Object>, ClaimsMutator<Claims> {
        String ISSUER = "iss";
        String SUBJECT = "sub";
        String AUDIENCE = "aud";
        String EXPIRATION = "exp";
        String NOT_BEFORE = "nbf";
        String ISSUED_AT = "iat";
        String ID = "jti";
    }
    
    
    @ConfigurationProperties(prefix = "jwt")
    @Component
    @Data
    public class JwtConfig {
        public static final String REDIS_TOKEN_KEY_PREFIX = "TOKEN_";	//Redis缓存前缀
        private long time = 432000;	//5天过期时间
        private String secret = "Sara";	//JWT密码
        private String prefix = "Bearer ";	//Token前缀
        private String header = "Authorization";	//存放Token的Header Key
    }
    
    
    @Component
    public class JwtTokenUtil implements Serializable {
    
        private static final long serialVersionUID = -5625635588908941275L;
    
        private static final String CLAIM_KEY_USERNAME = "sub";
        private static final String CLAIM_KEY_CREATED = "created";
        private static final String CLAIM_KEY_ROLES = "roles";
    
        @Autowired
        private JwtConfig jwtConfig;
    
    
        /**
         * 生成token
         */
        public String generateToken(UserDetails userDetails) {
            Map<String, Object> claims = new HashMap<>(3);
            claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());	//放入用户名 sub
            claims.put(CLAIM_KEY_CREATED, new Date());	//放入token生成时间 created
    
            List<String> roles = new ArrayList<>();
            Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                roles.add(authority.getAuthority());
            }
            claims.put(CLAIM_KEY_ROLES, roles);	//放入用户权限 roles
    
            return generateToken(claims);
        }
    
        /**
         * 生成token
         */
        private String generateToken(Map<String, Object> claims) {
            return Jwts.builder()
                    .setClaims(claims)
                    .setExpiration(new Date(System.currentTimeMillis() + jwtConfig.getTime() * 1000))
                    .signWith(SignatureAlgorithm.HS512, jwtConfig.getSecret())
                    .compact();
        }
    
        /**
         * 校验token
         */
        public Boolean validateToken(String token, UserDetails userDetails) {
            final String username = getUsernameFromToken(token);	//从token中取出用户名
            return (username.equals(userDetails.getUsername())
                    &&
                    !isTokenExpired(token)	//校验是否过期
            );
        }
    
    
        /**
         * 从request中获取username
         */
        public String getUsernameFromRequest(HttpServletRequest request) {
            String token = request.getHeader(jwtConfig.getHeader());
            token = token.substring(jwtConfig.getPrefix().length());
    
            return token == null ? null : getUsernameFromToken(token);
        }
    
        /**
         * 从token中获取username
         */
        public String getUsernameFromToken(String token) {
            String username;
            try {
                final Claims claims = getClaimsFromToken(token);
                username = claims.getSubject();
            } catch (Exception e) {
                username = null;
            }
            return username;
        }
    
        /**
         * 从token中获取Claims
         */
        private Claims getClaimsFromToken(String token) {
            Claims claims;
            try {
                claims = Jwts.parser()
                        .setSigningKey(jwtConfig.getSecret())
                        .parseClaimsJws(token)
                        .getBody();
            } catch (Exception e) {
                claims = null;
            }
            return claims;
        }
    
        /**
         * 从token中获取创造时间
         */
        public Date getCreatedDateFromToken(String token) {
            Date created;
            try {
                final Claims claims = getClaimsFromToken(token);
                created = new Date((Long)claims.get(CLAIM_KEY_CREATED));
            } catch (Exception e) {
                created = null;
            }
            return created;
        }
    
        /**
         * token是否过期
         * 
         * true 过期 false 未过期
         */
        public Boolean isTokenExpired(String token) {
            final Date expiration = getExpirationDateFromToken(token);
            boolean isExpired = expiration.before(new Date());
            return isExpired;
        }
    
        /**
         * 从token中获取过期时间
         */
        public Date getExpirationDateFromToken(String token) {
            Date expiration;
            try {
                final Claims claims = getClaimsFromToken(token);
                expiration = claims.getExpiration();
            } catch (Exception e) {
                expiration = null;
            }
            return expiration;
        }
    
        /**
         * 从token中获取用户角色
         */
        public List<String> getRolesFromToken(String authToken) {
            List<String> roles;
            try {
                final Claims claims = getClaimsFromToken(authToken);
                roles = (List<String>)claims.get(CLAIM_KEY_ROLES);
            } catch (Exception e) {
                roles = null;
            }
            return roles;
        }
    }
    
  • 相关阅读:
    SQL进阶总结(二)
    SQL进阶总结(一)
    Springmvc学习(三)——响应数据和结果视图
    springMVC学习(二)——常用注解
    Springmvc学习(一)——简单入门案例
    Nginx解决前端访问资源跨域问题
    MySQL-安装配置篇
    Centos下的MySQL安装及配置
    关于Java 值传递深度分析
    c#统计代码行数
  • 原文地址:https://www.cnblogs.com/loveer/p/12081100.html
Copyright © 2020-2023  润新知