springboot+springSecurity+JWT实现注册登录
1.数据库表
CREATE TABLE `user` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `username` varchar(60) COLLATE utf8_bin DEFAULT NULL, `password` varchar(60) COLLATE utf8_bin DEFAULT NULL, `role` varchar(15) COLLATE utf8_bin DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
2.工程结果如下:
3.依赖
<dependencies> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.security.oauth/spring-security-oauth2 --> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.3.5.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-jwt --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.10.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.security.oauth.boot/spring-security-oauth2-autoconfigure --> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.4.RELEASE</version> </dependency> <!-- mybatis依赖 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- redis依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies>
4.properties文件
spring.datasource.url=jdbc:mysql://localhost:3306/vue?useUnicode=true spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.username=root spring.datasource.password=root #redis配置 #spring.session.store-type=redis #spring.redis.database=0 #spring,redis.host=127.0.0.1 #spring.redis.port=6379 #spring.redis.pool.min-idle=10000 #spring.redis.timeout=30000 #mybatis配置 mybatis.type-aliases-package=com.example.security.entity mybatis.mapper-locations=classpath:mapper/*.xml #jwt设置 # JWT存储的请求头 jwt.tokenHeader=Authorization # JWT 加解密使用的密钥 jwt.secret=yeb-secret # JWT的超期限时间(60*60*24) jwt.expiration=604800 # JWT 负载中拿到开头 jwt.tokenHead=Bearer
5.JwtToken工具类
@Component public class JwtTokenUtil { private static final String CLAIM_KEY_USERNAME = "sub"; private static final String CLAIM_KEY_CREATED = "created"; @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; /** * 根据用户信息生成token * * @param userDetails * @return */ public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } /** * 从token中获取登录用户名 * @param token * @return */ public String getUserNameFromToken(String token){ String username; try { Claims claims = getClaimsFormToken(token); username = claims.getSubject(); } catch (Exception e) { username = null; } return username; } /** * 验证token是否有效 * @param token * @param userDetails * @return */ public boolean validateToken(String token,UserDetails userDetails){ String username = getUserNameFromToken(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } /** * 判断token是否可以被刷新 * @param token * @return */ public boolean canRefresh(String token){ return !isTokenExpired(token); } /** * 刷新token * @param token * @return */ public String refreshToken(String token){ Claims claims = getClaimsFormToken(token); claims.put(CLAIM_KEY_CREATED,new Date()); return generateToken(claims); } /** * 判断token是否失效 * @param token * @return */ private boolean isTokenExpired(String token) { Date expireDate = getExpiredDateFromToken(token); return expireDate.before(new Date()); } /** * 从token中获取过期时间 * @param token * @return */ private Date getExpiredDateFromToken(String token) { Claims claims = getClaimsFormToken(token); return claims.getExpiration(); } /** * 从token中获取荷载 * @param token * @return */ private Claims getClaimsFormToken(String token) { Claims claims = null; try { claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } catch (Exception e) { e.printStackTrace(); } return claims; } /** * 根据荷载生成JWT TOKEN * * @param claims * @return */ private String generateToken(Map<String, Object> claims) { return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } /** * 生成token失效时间 * * @return */ private Date generateExpirationDate() { return new Date(System.currentTimeMillis() + expiration * 1000); } }
6.身份验证入口点(当未登录或者token失效时候返回结果)
@Component public class RestAuthorizationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); PrintWriter out = response.getWriter(); Map<String, Object> map = new HashMap<>(); map.put("code", 401); map.put("message", "尚未登录,请登录!"); out.write(new ObjectMapper().writeValueAsString(map)); out.flush(); out.close(); } }
7.访问被拒绝处理程序(当访问接口没有权限时,自定义返回结果)
@Component public class RestfulAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); PrintWriter out = response.getWriter(); Map<String, Object> map = new HashMap<>(); map.put("code", 403); map.put("message", "权限不足,请联系管理员!"); out.write(new ObjectMapper().writeValueAsString(map)); out.flush(); out.close(); } }
8.JWT登录授权过滤器
public class JwtAuthencationTokenFilter extends OncePerRequestFilter { @Value("${jwt.tokenHeader}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private UserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authHeader = request.getHeader(tokenHeader); //存在token if (null != authHeader && authHeader.startsWith(tokenHead)) { String authToken = authHeader.substring(tokenHead.length()); String username = jwtTokenUtil.getUserNameFromToken(authToken); //token存在用户名但未登录 if (null != username && null == SecurityContextHolder.getContext().getAuthentication()) { //登录 UserDetails userDetails = userDetailsService.loadUserByUsername(username); //验证token是否有效,重新设置用户对象 if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } } } filterChain.doFilter(request, response); } }
9.Security配置类
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private IUserService userService; @Autowired private RestAuthorizationEntryPoint restAuthorizationEntryPoint; @Autowired private RestfulAccessDeniedHandler restfulAccessDeniedHandler; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder()); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers( "/register", "/login", "/logout", "/css/**", "/js/**", "/index.html", "favicon.ico", "/doc.html", "/webjars/**", "/swagger-resources/**", "/v2/api-docs/**", "/captcha", "/ws/**" ); } @Override protected void configure(HttpSecurity http) throws Exception { //使用JWT,不需要csrf http.csrf() .disable() //基于token,不需要session .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() //所有请求都要求认证 .anyRequest() .authenticated() //动态权限配置 .and() //禁用缓存 .headers() .cacheControl(); //添加jwt 登录授权过滤器 http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class); //添加自定义未授权和未登录结果返回 http.exceptionHandling() .accessDeniedHandler(restfulAccessDeniedHandler) .authenticationEntryPoint(restAuthorizationEntryPoint); } @Override @Bean public UserDetailsService userDetailsService(){ return username -> { JwtUser admin = userService.getUserByName(username); if (null!=admin){ return admin; } throw new UsernameNotFoundException("用户名或密码不正确"); }; } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Bean public JwtAuthencationTokenFilter jwtAuthencationTokenFilter(){ return new JwtAuthencationTokenFilter(); } }
10.实体类
** * @author hyy * @ClassName JwtUserService.java * @Description TODO 该类封装登录用户相关信息,例如用户名,密码,权限集合等,需要实现UserDetails 接口, * @createTime 2021年08月03日 */ public class JwtUser implements UserDetails { private Integer id; private String username; private String password; private Collection<? extends GrantedAuthority> authorities; public JwtUser() { } // 写一个能直接使用user创建jwtUser的构造器 public JwtUser(User user) { id = user.getId(); username = user.getUsername(); password = user.getPassword(); authorities = Collections.singleton(new SimpleGrantedAuthority(user.getRole())); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return false; } @Override public boolean isAccountNonLocked() { return false; } @Override public boolean isCredentialsNonExpired() { return false; } @Override public boolean isEnabled() { return false; } @Override public String toString() { return "JwtUserService{" + "id=" + id + ", username='" + username + '\'' + ", password='" + password + '\'' + ", authorities=" + authorities + '}'; } }
/** * user * @author */ @Data public class User implements Serializable { private Integer id; private String username; private String password; private String role; private static final long serialVersionUID = 1L; }
/** * @author hyy * @ClassName LoginDto.java * @Description TODO 获取前端登录信息 * @createTime 2021年08月03日 */ @Data @AllArgsConstructor @NoArgsConstructor public class LoginDto { private String username; private String password; }
11.dao.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.example.security.dao.UserDao"> <resultMap id="BaseResultMap" type="com.example.security.entity.User"> <id column="id" jdbcType="INTEGER" property="id" /> <result column="username" jdbcType="VARCHAR" property="username" /> <result column="password" jdbcType="VARCHAR" property="password" /> <result column="role" jdbcType="VARCHAR" property="role" /> </resultMap> <sql id="Base_Column_List"> id, username, `password`, `role` </sql> <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap"> select <include refid="Base_Column_List" /> from user where id = #{id,jdbcType=INTEGER} </select> <select id="getUserByName" parameterType="String" resultMap="BaseResultMap"> select <include refid="Base_Column_List" /> from user where username = #{userName,jdbcType=VARCHAR} </select> <delete id="deleteByPrimaryKey" parameterType="java.lang.Integer"> delete from user where id = #{id,jdbcType=INTEGER} </delete> <insert id="insert" keyColumn="id" keyProperty="id" parameterType="com.example.security.entity.User" useGeneratedKeys="true"> insert into user (username, `password`, `role` ) values (#{username,jdbcType=VARCHAR}, #{password,jdbcType=VARCHAR}, #{role,jdbcType=VARCHAR} ) </insert> <insert id="insertSelective" keyColumn="id" keyProperty="id" parameterType="com.example.security.entity.User" useGeneratedKeys="true"> insert into user <trim prefix="(" suffix=")" suffixOverrides=","> <if test="username != null"> username, </if> <if test="password != null"> `password`, </if> <if test="role != null"> `role`, </if> </trim> <trim prefix="values (" suffix=")" suffixOverrides=","> <if test="username != null"> #{username,jdbcType=VARCHAR}, </if> <if test="password != null"> #{password,jdbcType=VARCHAR}, </if> <if test="role != null"> #{role,jdbcType=VARCHAR}, </if> </trim> </insert> <update id="updateByPrimaryKeySelective" parameterType="com.example.security.entity.User"> update user <set> <if test="username != null"> username = #{username,jdbcType=VARCHAR}, </if> <if test="password != null"> `password` = #{password,jdbcType=VARCHAR}, </if> <if test="role != null"> `role` = #{role,jdbcType=VARCHAR}, </if> </set> where id = #{id,jdbcType=INTEGER} </update> <update id="updateByPrimaryKey" parameterType="com.example.security.entity.User"> update user set username = #{username,jdbcType=VARCHAR}, `password` = #{password,jdbcType=VARCHAR}, `role` = #{role,jdbcType=VARCHAR} where id = #{id,jdbcType=INTEGER} </update> </mapper>
12.dao层
@Mapper public interface UserDao { int deleteByPrimaryKey(Integer id); int insert(User record); int insertSelective(User record); User selectByPrimaryKey(Integer id); int updateByPrimaryKeySelective(User record); int updateByPrimaryKey(User record); User getUserByName(String userName); }
13.service
public interface IUserService { int insertSelective(User record); JwtUser getUserByName(String userName); Map login(String username, String password, HttpServletRequest request); } @Service public class UserServiceImpl implements IUserService { @Autowired private UserDao userDao; @Autowired private UserDetailsService userDetailsService; @Autowired private PasswordEncoder passwordEncoder; @Autowired private JwtTokenUtil jwtTokenUtil; @Value("${jwt.tokenHead}") private String tokenHead; @Override public int insertSelective(User record) { return userDao.insertSelective(record); } public JwtUser getUserByName(String userName) { User user = userDao.getUserByName(userName); JwtUser jwtUser = new JwtUser(user); return jwtUser; } @Override public Map login(String username, String password, HttpServletRequest request) { Map<String, Object> tokenMap = new HashMap<>(); //登录 UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (null == userDetails || !passwordEncoder.matches(password, userDetails.getPassword())) { tokenMap.put("code", 400); tokenMap.put("message", "密码错误"); return tokenMap; } //更新security登录用户对象 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails , null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); //生成token String token = jwtTokenUtil.generateToken(userDetails); tokenMap.put("token", token); tokenMap.put("tokenHead", tokenHead); tokenMap.put("code", 200); tokenMap.put("message", "登录成功"); return tokenMap; } }
14Controller层
@RestController public class AuthController { @Autowired private IUserService userService; @PostMapping("/register") public String registerUser(@RequestBody Map<String, String> registerUser) { User user = new User(); BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); user.setUsername(registerUser.get("username")); user.setPassword(bCryptPasswordEncoder.encode(registerUser.get("password"))); user.setRole(registerUser.get("role")); int i = userService.insertSelective(user); if (i < 1) { return "error"; } return "success"; } @PostMapping("/login") public Map login(@RequestBody LoginDto adminLoginParam, HttpServletRequest request) { return userService.login(adminLoginParam.getUsername(), adminLoginParam.getPassword(), request); } //测试只有登录拿到token才能拿到数据 @GetMapping("/get") public String aaa() { return "token验证通过了";
} }
15.在postman上验证注册
数据库能查询到刚刚注册的信息,密码后台已经加密了
16.登录,获取到token信息
17.验证没有token或者token错误下,请求其他接口会失败
a.token是错误的
b.token是正确的,能够返回正确的信息