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;
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM